package cn.com.greatwall.flowable.service.impl;

import java.io.InputStream;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.ObjectUtils;
import org.flowable.common.engine.api.FlowableException;
import org.flowable.engine.ManagementService;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.engine.repository.ProcessDefinitionQuery;
import org.flowable.identitylink.api.IdentityLink;
import org.flowable.job.api.Job;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import cn.com.greatwall.common.utils.PageUtil;
import cn.com.greatwall.common.utils.SecurityUtils;
import cn.com.greatwall.flowable.common.cmd.GetProcessDefinitionInfoCmd;
import cn.com.greatwall.flowable.common.constant.FlowableConstant;
import cn.com.greatwall.flowable.service.PermissionService;
import cn.com.greatwall.flowable.service.ProcessDefinitionService;
import cn.com.greatwall.flowable.service.dto.IdentityLinkDto;
import cn.com.greatwall.flowable.service.dto.ProcessDefinitionDto;
import cn.com.greatwall.flowable.service.dto.criteria.ProcessDefinitionQueryCriteria;
import cn.com.greatwall.flowable.service.mapstruct.IdentityLinkMapper;
import cn.com.greatwall.flowable.service.mapstruct.ProcessDefinitionMapper;
import lombok.RequiredArgsConstructor;

/**
 * @Author herw
 * @Time 2020-11-17 14:47:09
 * @Version 1.0
 * @Description: TODO(用一句话描述该文件做什么)
 */
@Service
@RequiredArgsConstructor
public class ProcessDefinitionServiceImpl implements ProcessDefinitionService {
    private final RepositoryService repositoryService;
    private final ManagementService managementService;
    private final RuntimeService runtimeService;
    private final PermissionService permissionService;
    private final ProcessDefinitionMapper processDefinitionMapper;
    private final IdentityLinkMapper identityLinkMapper;

    @Override
    public Map<String, Object> queryAll(ProcessDefinitionQueryCriteria criteria, Pageable pageable) {
        ProcessDefinitionQuery query = repositoryService.createProcessDefinitionQuery();
        if (ObjectUtils.isNotEmpty(criteria.getId())) {
            query.processDefinitionId(criteria.getId());
        }
        if (ObjectUtils.isNotEmpty(criteria.getCategory())) {
            query.processDefinitionCategoryLike("%" + criteria.getCategory() + "%");
        }
        if (ObjectUtils.isNotEmpty(criteria.getKey())) {
            query.processDefinitionKeyLike("%" + criteria.getKey() + "%");
        }
        if (ObjectUtils.isNotEmpty(criteria.getName())) {
            query.processDefinitionNameLike("%" + criteria.getName() + "%");
        }
        if (ObjectUtils.isNotEmpty(criteria.getVersion())) {
            query.processDefinitionVersion(criteria.getVersion());
        }
        if (ObjectUtils.isNotEmpty(criteria.getSuspened())) {
            if (criteria.getSuspened()) {
                query.suspended();
            } else {
                query.active();
            }
        }
        if (ObjectUtils.isNotEmpty(criteria.getStartableByUser())) {
            query.startableByUser(criteria.getStartableByUser());
        }
        if (ObjectUtils.isNotEmpty(criteria.getTenantId())) {
            query.processDefinitionTenantId(criteria.getTenantId());
        }

        List<ProcessDefinition> list = query.listPage((int) pageable.getOffset(),
                pageable.getPageSize());

        return PageUtil.toPage(processDefinitionMapper.toDto(list), query.count());
    }

    @Override
    public Map<String, Object> queryStartableUser(ProcessDefinitionQueryCriteria criteria, Pageable pageable) {
        ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery();
        if (ObjectUtils.isNotEmpty(criteria.getName())) {
            processDefinitionQuery.processDefinitionNameLike("%" + criteria.getName() + "%");
        }
        processDefinitionQuery.latestVersion().active().startableByUser(SecurityUtils.getCurrentUserId().toString());

        List<ProcessDefinition> list = processDefinitionQuery.listPage((int) pageable.getOffset(),
                pageable.getPageSize());

        return PageUtil.toPage(processDefinitionMapper.toDto(list), processDefinitionQuery.count());
    }

    @Override
    public ProcessDefinition findById(String processDefinitionId) {
        return managementService.executeCommand(new GetProcessDefinitionInfoCmd(processDefinitionId, null, null));
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void delete(String processDefinitionId, Boolean cascade) {
        ProcessDefinition processDefinition = findById(processDefinitionId);
        if (processDefinition.getDeploymentId() == null) {
            throw new FlowableException("Process definition has not deployed with id " + processDefinitionId);
        }
        if (cascade) {
            List<Job> jobs = managementService.createTimerJobQuery().processDefinitionId(processDefinitionId).list();
            for (Job job : jobs) {
                managementService.deleteTimerJob(job.getId());
            }
            repositoryService.deleteDeployment(processDefinition.getDeploymentId(), true);
        } else {
            Long processCount = runtimeService.createProcessInstanceQuery().processDefinitionId(processDefinitionId)
                    .count();
            if (processCount > 0) {
                throw new FlowableException(
                        "There are running instances with process definition id " + processDefinitionId);
            }

            Long jobCount = managementService.createTimerJobQuery().processDefinitionId(processDefinitionId).count();
            if (jobCount > 0) {
                throw new FlowableException(
                        "There are running time jobs with process definition id " + processDefinitionId);
            }
            repositoryService.deleteDeployment(processDefinition.getDeploymentId());
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void activate(ProcessDefinitionDto dto) {
        String processDefinitionId = dto.getId();
        ProcessDefinition processDefinition = findById(processDefinitionId);
        if (!processDefinition.isSuspended()) {
            throw new FlowableException("Process definition is not suspended with id " + processDefinitionId);
        }
        repositoryService.activateProcessDefinitionById(processDefinitionId, dto.isIncludeProcessInstances(),
                dto.getDate());
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void suspend(ProcessDefinitionDto dto) {
        String processDefinitionId = dto.getId();
        ProcessDefinition processDefinition = findById(processDefinitionId);
        if (processDefinition.isSuspended()) {
            throw new FlowableException("Process definition is already suspended with id " + processDefinitionId);
        }
        repositoryService.suspendProcessDefinitionById(processDefinitionId, dto.isIncludeProcessInstances(),
                dto.getDate());
    }

    @Override
    public InputStream exportXML(String processDefinitionId) throws FlowableException {
        permissionService.validateReadPermissionOnProcessDefinition(SecurityUtils.getCurrentUserId().toString(),
                processDefinitionId);
        ProcessDefinition processDefinition = findById(processDefinitionId);
        String deploymentId = processDefinition.getDeploymentId();
        String resourceId = processDefinition.getResourceName();
        if (deploymentId == null || deploymentId.length() == 0) {
            throw new FlowableException("Process definition deployment id is not found with id " + processDefinitionId);
        }
        if (resourceId == null || resourceId.length() == 0) {
            throw new FlowableException("Process definition resource id is not found with id " + processDefinitionId);
        }
        Deployment deployment = repositoryService.createDeploymentQuery().deploymentId(deploymentId).singleResult();
        if (deployment == null) {
            throw new FlowableException("Process definition deployment is not found with deploymentId " + deploymentId);
        }
        List resourceList = repositoryService.getDeploymentResourceNames(deploymentId);
        if (ObjectUtils.isEmpty(resourceList) || !resourceList.contains(resourceId)) {
            throw new FlowableException("Process definition resourceId " + resourceId + " is not found with "
                    + "deploymentId " + deploymentId);
        }

        InputStream resourceStream = repositoryService.getResourceAsStream(deploymentId, resourceId);

        return resourceStream;
    }

    @Override
    public List<IdentityLinkDto> queryProcessDefinitionIdentityLink(String processDefinitionId) {
        ProcessDefinition processDefinition = findById(processDefinitionId);
        List<IdentityLink> list = repositoryService.getIdentityLinksForProcessDefinition(processDefinition.getId());
        return identityLinkMapper.toDto(list);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saveProcessDefinitionIdentityLink(IdentityLinkDto dto) {
        validateIdentityArguments(dto.getIdentityId(), dto.getIdentityType());
        ProcessDefinition processDefinition = findById(dto.getProcessDefinitionId());
        if (FlowableConstant.IDENTITY_GROUP.equals(dto.getIdentityType())) {
            repositoryService.addCandidateStarterGroup(processDefinition.getId(), dto.getIdentityId());
        } else if (FlowableConstant.IDENTITY_USER.equals(dto.getIdentityType())) {
            repositoryService.addCandidateStarterUser(processDefinition.getId(), dto.getIdentityId());
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteProcessDefinitionIdentityLink(String processDefinitionId, String identityId,
            String identityType) {
        validateIdentityArguments(identityId, identityType);
        ProcessDefinition processDefinition = findById(processDefinitionId);
        if (FlowableConstant.IDENTITY_GROUP.equals(identityType)) {
            repositoryService.deleteCandidateStarterGroup(processDefinitionId, identityId);
        } else if (FlowableConstant.IDENTITY_USER.equals(identityType)) {
            repositoryService.deleteCandidateStarterUser(processDefinition.getId(), identityId);
        }
    }

    private void validateIdentityArguments(String identityId, String identityType) {
        if (identityId == null || identityId.length() == 0) {
            throw new FlowableException("IdentityId may not be null");
        }
        if (!FlowableConstant.IDENTITY_GROUP.equals(identityType)
                && !FlowableConstant.IDENTITY_USER.equals(identityType)) {
            throw new FlowableException(
                    "Type must be " + FlowableConstant.IDENTITY_GROUP + " or " + FlowableConstant.IDENTITY_USER);
        }
    }
}
